/**
 * Copyright 2012-2020 The Feign Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */
package feign;

import static feign.Util.checkArgument;
import static feign.Util.checkNotNull;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import feign.InvocationHandlerFactory.MethodHandler;
import feign.Param.Expander;
import feign.Request.Options;
import feign.codec.Decoder;
import feign.codec.EncodeException;
import feign.codec.Encoder;
import feign.codec.ErrorDecoder;
import feign.template.UriUtils;

public class ReflectiveFeign extends Feign {

	private final ParseHandlersByName targetToHandlersByName;
	private final InvocationHandlerFactory factory;
	private final QueryMapEncoder queryMapEncoder;

	ReflectiveFeign(ParseHandlersByName targetToHandlersByName, InvocationHandlerFactory factory, QueryMapEncoder queryMapEncoder) {
		this.targetToHandlersByName = targetToHandlersByName;
		this.factory = factory;
		this.queryMapEncoder = queryMapEncoder;
	}

	/**主要做两件事：
	 * <br/>1、扫描FeignClient接口类的所有函数，生成对应的Handler
	 * <br/>2、使用Proxy生成FeignClient的实例对象
	 * <br/>creates an api binding to the {@code target}. As this invokes reflection, care should be taken to cache the result.
	 */
	@SuppressWarnings("unchecked")
	@Override
	public <T> T newInstance(Target<T> target) {
		// Contract将target.type接口类上的方法和注解解析成MethodMetadata，并转换成内部的MethodHandler处理方式
		Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
		Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
		List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();

		for (Method method : target.type().getMethods()) {
			if (method.getDeclaringClass() == Object.class) {
				continue;
			} else if (Util.isDefault(method)) {
				DefaultMethodHandler handler = new DefaultMethodHandler(method);
				defaultMethodHandlers.add(handler);
				methodToHandler.put(method, handler);
			} else {
				methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));
			}
		}
		InvocationHandler handler = factory.create(target, methodToHandler);
		T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(), new Class<?>[] { target.type() }, handler);

		for (DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {
			defaultMethodHandler.bindTo(proxy);
		}
		return proxy;
	}

	static class FeignInvocationHandler implements InvocationHandler {

		private final Target target;
		private final Map<Method, MethodHandler> dispatch;

		FeignInvocationHandler(Target target, Map<Method, MethodHandler> dispatch) {
			this.target = checkNotNull(target, "target");
			this.dispatch = checkNotNull(dispatch, "dispatch for %s", target);
		}

		@Override
		public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
			if ("equals".equals(method.getName())) {
				try {
					Object otherHandler = args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
					return equals(otherHandler);
				} catch (IllegalArgumentException e) {
					return false;
				}
			} else if ("hashCode".equals(method.getName())) {
				return hashCode();
			} else if ("toString".equals(method.getName())) {
				return toString();
			}

			return dispatch.get(method).invoke(args);
		}

		@Override
		public boolean equals(Object obj) {
			if (obj instanceof FeignInvocationHandler) {
				FeignInvocationHandler other = (FeignInvocationHandler) obj;
				return target.equals(other.target);
			}
			return false;
		}

		@Override
		public int hashCode() {
			return target.hashCode();
		}

		@Override
		public String toString() {
			return target.toString();
		}
	}

	static final class ParseHandlersByName {

		private final Contract contract;
		private final Options options;
		private final Encoder encoder;
		private final Decoder decoder;
		private final ErrorDecoder errorDecoder;
		private final QueryMapEncoder queryMapEncoder;
		private final SynchronousMethodHandler.Factory factory;

		ParseHandlersByName(Contract contract, Options options, Encoder encoder, Decoder decoder, QueryMapEncoder queryMapEncoder, ErrorDecoder errorDecoder, SynchronousMethodHandler.Factory factory) {
			this.contract = contract;
			this.options = options;
			this.factory = factory;
			this.errorDecoder = errorDecoder;
			this.queryMapEncoder = queryMapEncoder;
			this.encoder = checkNotNull(encoder, "encoder");
			this.decoder = checkNotNull(decoder, "decoder");
		}

		/**Contract统一将方法解析MethodMetadata(*)，这样就可以通过实现不同的Contract适配各种REST声明式规范
		 * <br/>buildTemplate实际上将Method方法的参数转换成 Request
		 * <br/>将metadata和buildTemplate封装成MethodHandler
		 * <br/>这样通过以上三步就创建了一个Target.type的代理对象proxy，这个代理对象就可以像访问普通方法一样发送Http请求，其实和RPC的Stub模型是一样的。了解proxy后，其执行过程其实也就一模了然
		 * @param target
		 * @return
		 */
		public Map<String, MethodHandler> apply(Target target) {
			// contract将接口类中的方法和注解解析MethodMetadata
			List<MethodMetadata> metadata = contract.parseAndValidateMetadata(target.type());
			Map<String, MethodHandler> result = new LinkedHashMap<String, MethodHandler>();
			for (MethodMetadata md : metadata) {
				// buildTemplate 实际上将 Method 方法的参数转换成 Request
				BuildTemplateByResolvingArgs buildTemplate;
				if (!md.formParams().isEmpty() && md.template().bodyTemplate() == null) {
					// 表单
					buildTemplate = new BuildFormEncodedTemplateFromArgs(md, encoder, queryMapEncoder, target);
				} else if (md.bodyIndex() != null) {
					// 注解
					buildTemplate = new BuildEncodedTemplateFromArgs(md, encoder, queryMapEncoder, target);
				} else {
					// 其余
					buildTemplate = new BuildTemplateByResolvingArgs(md, queryMapEncoder, target);
				}
				if (md.isIgnored()) {
					result.put(md.configKey(), args -> {
						throw new IllegalStateException(md.configKey() + " is not a method handled by feign");
					});
				} else {
					// 将 metadata 和 buildTemplate 封装成 MethodHandler
					result.put(md.configKey(), factory.create(target, md, buildTemplate, options, decoder, errorDecoder));
				}
			}
			return result;
		}
	}

	private static class BuildTemplateByResolvingArgs implements RequestTemplate.Factory {

		private final QueryMapEncoder queryMapEncoder;

		protected final MethodMetadata metadata;
		protected final Target<?> target;
		private final Map<Integer, Expander> indexToExpander = new LinkedHashMap<Integer, Expander>();

		private BuildTemplateByResolvingArgs(MethodMetadata metadata, QueryMapEncoder queryMapEncoder, Target target) {
			this.metadata = metadata;
			this.target = target;
			this.queryMapEncoder = queryMapEncoder;
			if (metadata.indexToExpander() != null) {
				indexToExpander.putAll(metadata.indexToExpander());
				return;
			}
			if (metadata.indexToExpanderClass().isEmpty()) {
				return;
			}
			for (Entry<Integer, Class<? extends Expander>> indexToExpanderClass : metadata.indexToExpanderClass().entrySet()) {
				try {
					indexToExpander.put(indexToExpanderClass.getKey(), indexToExpanderClass.getValue().newInstance());
				} catch (InstantiationException e) {
					throw new IllegalStateException(e);
				} catch (IllegalAccessException e) {
					throw new IllegalStateException(e);
				}
			}
		}

		@Override
		public RequestTemplate create(Object[] argv) {
			RequestTemplate mutable = RequestTemplate.from(metadata.template());
			mutable.feignTarget(target);
			if (metadata.urlIndex() != null) {
				// 从Contract的分析可知这里的urlIndex，代表的是方法中的参数可以是一个URI。
				// 会在这里抽取出来被拼接到原本的url的前面。
				// 但是我翻遍了wiki也没发现这样的用法的讲解。而且想想这样用不是很麻烦么，估计是以前的遗留功能，后来没用了吧。
				int urlIndex = metadata.urlIndex();
				checkArgument(argv[urlIndex] != null, "URI parameter %s was null", urlIndex);
				mutable.target(String.valueOf(argv[urlIndex]));
			}
			Map<String, Object> varBuilder = new LinkedHashMap<String, Object>();
			// 通过indexToName，解析Param注解了的参数的值，特别是自定义了Expander的，将在这替换成真正的值，存入varBuilder。key是注解里面的名字，value是真正展开后的值
			for (Entry<Integer, Collection<String>> entry : metadata.indexToName().entrySet()) {
				int i = entry.getKey();
				Object value = argv[entry.getKey()];
				if (value != null) { // Null values are skipped.
					if (indexToExpander.containsKey(i)) {
						value = expandElements(indexToExpander.get(i), value);
					}
					for (String name : entry.getValue()) {
						varBuilder.put(name, value);
					}
				}
			}

			RequestTemplate template = resolve(argv, mutable, varBuilder);
			if (metadata.queryMapIndex() != null) {
				// add query map parameters after initial resolve so that they take
				// precedence over any predefined values
				Object value = argv[metadata.queryMapIndex()];
				Map<String, Object> queryMap = toQueryMap(value);
				template = addQueryMapQueryParameters(queryMap, template);
			}

			if (metadata.headerMapIndex() != null) {
				template = addHeaderMapHeaders((Map<String, Object>) argv[metadata.headerMapIndex()], template);
			}

			return template;
		}

		private Map<String, Object> toQueryMap(Object value) {
			if (value instanceof Map) {
				return (Map<String, Object>) value;
			}
			try {
				return queryMapEncoder.encode(value);
			} catch (EncodeException e) {
				throw new IllegalStateException(e);
			}
		}

		private Object expandElements(Expander expander, Object value) {
			if (value instanceof Iterable) {
				return expandIterable(expander, (Iterable) value);
			}
			return expander.expand(value);
		}

		private List<String> expandIterable(Expander expander, Iterable value) {
			List<String> values = new ArrayList<String>();
			for (Object element : value) {
				if (element != null) {
					values.add(expander.expand(element));
				}
			}
			return values;
		}

		@SuppressWarnings("unchecked")
		private RequestTemplate addHeaderMapHeaders(Map<String, Object> headerMap, RequestTemplate mutable) {
			for (Entry<String, Object> currEntry : headerMap.entrySet()) {
				Collection<String> values = new ArrayList<String>();

				Object currValue = currEntry.getValue();
				if (currValue instanceof Iterable<?>) {
					Iterator<?> iter = ((Iterable<?>) currValue).iterator();
					while (iter.hasNext()) {
						Object nextObject = iter.next();
						values.add(nextObject == null ? null : nextObject.toString());
					}
				} else {
					values.add(currValue == null ? null : currValue.toString());
				}

				mutable.header(currEntry.getKey(), values);
			}
			return mutable;
		}

		@SuppressWarnings("unchecked")
		private RequestTemplate addQueryMapQueryParameters(Map<String, Object> queryMap, RequestTemplate mutable) {
			for (Entry<String, Object> currEntry : queryMap.entrySet()) {
				Collection<String> values = new ArrayList<String>();

				boolean encoded = metadata.queryMapEncoded();
				Object currValue = currEntry.getValue();
				if (currValue instanceof Iterable<?>) {
					Iterator<?> iter = ((Iterable<?>) currValue).iterator();
					while (iter.hasNext()) {
						Object nextObject = iter.next();
						values.add(nextObject == null ? null : encoded ? nextObject.toString() : UriUtils.encode(nextObject.toString()));
					}
				} else {
					values.add(currValue == null ? null : encoded ? currValue.toString() : UriUtils.encode(currValue.toString()));
				}

				mutable.query(encoded ? currEntry.getKey() : UriUtils.encode(currEntry.getKey()), values);
			}
			return mutable;
		}

		protected RequestTemplate resolve(Object[] argv, RequestTemplate mutable, Map<String, Object> variables) {
			return mutable.resolve(variables);
		}
	}

	private static class BuildFormEncodedTemplateFromArgs extends BuildTemplateByResolvingArgs {

		private final Encoder encoder;

		private BuildFormEncodedTemplateFromArgs(MethodMetadata metadata, Encoder encoder, QueryMapEncoder queryMapEncoder, Target target) {
			super(metadata, queryMapEncoder, target);
			this.encoder = encoder;
		}

		@Override
		protected RequestTemplate resolve(Object[] argv, RequestTemplate mutable, Map<String, Object> variables) {
			Map<String, Object> formVariables = new LinkedHashMap<String, Object>();
			for (Entry<String, Object> entry : variables.entrySet()) {
				if (metadata.formParams().contains(entry.getKey())) {
					formVariables.put(entry.getKey(), entry.getValue());
				}
			}
			try {
				encoder.encode(formVariables, Encoder.MAP_STRING_WILDCARD, mutable);
			} catch (EncodeException e) {
				throw e;
			} catch (RuntimeException e) {
				throw new EncodeException(e.getMessage(), e);
			}
			return super.resolve(argv, mutable, variables);
		}
	}

	private static class BuildEncodedTemplateFromArgs extends BuildTemplateByResolvingArgs {

		private final Encoder encoder;

		private BuildEncodedTemplateFromArgs(MethodMetadata metadata, Encoder encoder, QueryMapEncoder queryMapEncoder, Target target) {
			super(metadata, queryMapEncoder, target);
			this.encoder = encoder;
		}

		@Override
		protected RequestTemplate resolve(Object[] argv, RequestTemplate mutable, Map<String, Object> variables) {
			Object body = argv[metadata.bodyIndex()];
			checkArgument(body != null, "Body parameter %s was null", metadata.bodyIndex());
			try {
				encoder.encode(body, metadata.bodyType(), mutable);
			} catch (EncodeException e) {
				throw e;
			} catch (RuntimeException e) {
				throw new EncodeException(e.getMessage(), e);
			}
			return super.resolve(argv, mutable, variables);
		}
	}
}
